#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <fcntl.h>
#include <poll.h>
#include <unistd.h>

#ifdef USE_CAPSICUM
#include <sys/capsicum.h>
#endif

#include <err.h>
#include <sysexits.h>

#include "au.h"
#include "read/afile.h"
#include "common.h"

#ifndef NAMEMAX
#define NAMEMAX 10
#endif
#ifndef CBSIZE
#define CBSIZE 64
#endif
#ifndef BSIZE
#define BSIZE 0x1000
#endif
#ifdef SIGINFO
#define IFINFO(x) (x)
#else
#define IFINFO(x)
#endif

enum parse_state {
	PARSE_DEFAULT,
	PARSE_SET,
	PARSE_NUMBER,
	PARSE_DECIMAL,
	PARSE_ZEE
};

enum {
	PARSE_SEEK = 1,
	PARSE_INFO,
	PARSE_PLAYPAUSE,
	PARSE_HZ,
	PARSE_QUIT
};

#define PARSE_INITIALIZER { { 0 }, { 0 }, PARSE_DEFAULT, 0, 0, 0, 0, 0 }
struct parse {
	char buffer[CBSIZE];
	struct timespec ts;
	enum parse_state state;
	int whence;
	int negative;
	int index;
	int count;
	int decimals;
};

#ifdef SIGINFO
static size_t info_bytes = 0;
#endif

#ifdef SIGINFO
static void	siginfo(int);
#endif
static void	printts(int, struct timespec);
static int	parsefd(int, struct parse *);

int
main(int argc, char *argv[])
{
#ifdef USE_CAPSICUM
	cap_rights_t rights;
#endif
#ifdef USE_MODE
	char *amode = NULL;
#endif
#ifdef USE_QUALITY
	double quality = 1;
#endif
	struct pollfd fds[2] = {
		{ STDIN_FILENO, POLLIN, 0 },
		{ -1, POLLIN, 0 }
	};
	struct parse parse = PARSE_INITIALIZER;
	struct au au = AU_INITIALIZER;
	struct afile *a;
	struct timespec ts;
	const char *cfile = NULL;
	const char *ifile = NULL;
	int cfd = -1;
	int ifd = -1;
	ssize_t i;
	ssize_t r;
	ssize_t t;
	size_t bsize = BSIZE;
	time_t ihz = -1;
	size_t bsi = 0;
	size_t bps;
	void *b;
	char *p;
	int vflag = 0;
	int first = 1;
	int c;

	while ((c = getopt(argc, argv, "C:I:b:c:i:v")) != -1)
		switch (c) {
		case 'C':
			cfile = NULL;
			cfd = strtoul(optarg, &p, 0);
			if (p == optarg || *p != '\0')
				errx(EX_USAGE, "Invalid number '%s'",
				    optarg);
			if (cfd == STDIN_FILENO ||
			    cfd == STDOUT_FILENO ||
			    cfd == STDERR_FILENO ||
			    fcntl(cfd, F_GETFL, &c) == -1)
				errx(EX_USAGE,
				    "Invalid file-descriptor %d", cfd);
			break;
		case 'I':
			ifile = NULL;
			ifd = strtoul(optarg, &p, 0);
			if (p == optarg || *p != '\0')
				errx(EX_USAGE, "Invalid number '%s'",
				    optarg);
			if (ifd == STDIN_FILENO ||
			    ifd == STDOUT_FILENO ||
			    ifd == STDERR_FILENO ||
			    fcntl(ifd, F_GETFL, &c) == -1)
				errx(EX_USAGE,
				    "Invalid file-descriptor %d", ifd);
			break;
		case 'b':
			bsize = strtoul(optarg, &p, 0);
			if (p == optarg || *p != '\0')
				errx(EX_USAGE, "Invalid number '%s'",
				    optarg);
			if (bsize < 0x400)
				errx(EX_USAGE, "The buffer size (%zu) "
				    "is too small", bsize);
			break;
		case 'c':
			cfd = -1;
			cfile = optarg;
			break;
		case 'i':
			ifd = -1;
			ifile = optarg;
			break;
		case 'v':
			vflag = 1;
			break;
		default:
			goto usage;
		}
	if ((b = malloc(bsize)) == NULL)
		err(EX_OSERR, "Could not allocate the buffer");
	IFINFO((void)signal(SIGINFO, &siginfo));
	if (cfile != NULL)
		/* Use O_RDWR to make poll(2) work well with FIFOs */
		if ((cfd = open(cfile, O_RDWR | O_CLOEXEC)) < 0)
			warn("Could not open '%s'", cfile);
	if (ifile != NULL)
		if ((ifd = open(ifile, O_APPEND | O_CREAT | O_CLOEXEC))
		    < 0)
			warn("Could not open '%s'", ifile);
#ifdef USE_PLEDGE
	pledge("stdio", NULL);
#endif
#ifdef USE_CAPSICUM
	if (cap_enter() == -1 && errno != ENOSYS)
		err(EX_OSERR, "Could not enter capability mode");
	(void)cap_rights_init(&rights, CAP_READ, CAP_EVENT, CAP_FCNTL,
	    CAP_FSTAT, CAP_SEEK);
	if (cap_rights_limit(STDIN_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard input");
	(void)cap_rights_init(&rights, CAP_EVENT, CAP_FCNTL, CAP_FSTAT,
	    CAP_READ, CAP_WRITE);
	if (cfd >= 0 && cap_rights_limit(cfd, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the command input");
	(void)cap_rights_init(&rights, CAP_WRITE);
	if (cap_rights_limit(STDERR_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard error");
	(void)cap_rights_set(&rights, CAP_EVENT, CAP_FCNTL);
	if (cap_rights_limit(STDOUT_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard output");
	if (ifd >= 0 && cap_rights_limit(ifd, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the informative output");
#endif /* USE_CAPSICUM */
	if ((a = af_fdopen(STDIN_FILENO)) == NULL)
		err(EX_DATAERR, "Could not initialize audio codec");
	if (setblock(STDIN_FILENO, 0) != 0 ||
	    setblock(STDOUT_FILENO, 0) != 0 ||
	    (cfd >= 0 && setblock(cfd, 0) != 0))
		err(EX_OSERR, "Could not set O_NONBLOCK");
	if (ifd < 0)
		ifd = STDERR_FILENO;
#ifdef USE_CAPSICUM
	/* CAP_FCNTL is no longer needed */
	(void)cap_rights_init(&rights, CAP_READ, CAP_EVENT, CAP_FSTAT,
	     CAP_SEEK);
	if (cap_rights_limit(STDIN_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard input");
	(void)cap_rights_init(&rights, CAP_WRITE, CAP_EVENT);
	if (cap_rights_limit(STDOUT_FILENO, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the standard output");
	(void)cap_rights_init(&rights, CAP_EVENT, CAP_READ, CAP_WRITE);
	if (cfd >= 0 && cap_rights_limit(cfd, &rights) == -1 &&
	    errno != ENOSYS)
		err(EX_OSERR, "Could not limit the command input");
#endif
	fds[1].fd = cfd;
	while ((c = poll(fds, 1 + (cfd >= 0), INFTIM)) >= 0 ||
	    errno == EINTR) {
		if (c <= 0)
			continue;
		if (fds[0].revents & POLLIN) {
			errno = 0;
			if (first)
				r = af_read(a, (char *)b + AU_SIZE,
				    bsize - AU_SIZE);
			else
				r = af_read(a, b, bsize);
			if (r == 0 || (r < 0 && errno != EAGAIN &&
			    errno != EINTR))
				break;
			if (r > 0) {
				if (first) {
					if (af_get(a,
					    "encoding rate channels",
					    &au.au_enc, &au.au_rate,
					    &au.au_chan) != 3)
						err(EX_SOFTWARE,
						    "Could not get "
						    "properties");
					if (cfd >= 0) {
						ts.tv_sec = 0;
						ts.tv_nsec = 0;
						ts = af_seek(a, ts,
						    SEEK_CUR);
					}
					if (cfd < 0 || ts.tv_sec < 0 ||
					    ts.tv_nsec < 0)
						(void)af_get(a, "size",
						    &au.au_size);
					(void)au_puthdr(&au, b);
					r += AU_SIZE;
					bps = au_ssize(&au) *
					    (size_t)au.au_rate;
					first = 0;
				}
				i = 0;
				fds[0].fd = STDOUT_FILENO;
				fds[0].events = POLLOUT;
			}
		} else if (fds[0].revents & POLLOUT) {
			if ((t = write(STDOUT_FILENO, (char *)b + i,
			    r - i)) == 0 || (r < 0 && errno != EAGAIN &&
			    errno != EINTR))
				break;
			IFINFO(info_bytes += t);
			if (r > 0 && (i += t) >= r) {
				if (vflag && ifd >= 0 &&
				    (ihz < 0 || (ihz > 0 &&
				    (bsi += t) > bps / ihz))) {
					bsi = 0;
					ts.tv_sec = ts.tv_nsec = 0;
					printts(ifd, af_seek(a, ts,
					    SEEK_CUR));
					(void)write(ifd, "/", 1);
					printts(ifd, af_length(a));
					(void)write(ifd, "\n", 1);
				}
				fds[0].fd = STDIN_FILENO;
				fds[0].events = POLLIN;
			}
		}
		if (fds[0].revents & (POLLHUP | POLLERR | POLLNVAL)) {
			/* Finish reading any buffered audio. */
			if ((r = af_read(a, b, bsize)) <= 0)
				break;
			fds[0].fd = STDOUT_FILENO;
			fds[0].events = POLLOUT;
		}
		if (cfd >= 0 && (fds[1].revents & POLLIN)) {
			while ((c = parsefd(cfd, &parse)) > 0)
				switch (c) {
				case PARSE_SEEK:
					ts = af_seek(a, parse.ts,
					    parse.whence);
					if (ts.tv_sec < 0 ||
					    ts.tv_nsec < 0) {
						if (errno == ERANGE)
							goto write_ret;
						warn("seek error");
					}
					if (vflag) {
						printts(ifd, ts);
						(void)write(ifd,
						    "/", 1);
						printts(ifd,
						    af_length(a));
						(void)write(ifd,
						    "\n", 1);
					}
					break;
				case PARSE_INFO:
					ts.tv_sec = ts.tv_nsec = 0;
					printts(ifd, af_seek(a, ts,
					    SEEK_CUR));
					(void)write(ifd, "/", 1);
					printts(ifd, af_length(a));
					(void)write(ifd, "\n", 1);
					break;
				case PARSE_PLAYPAUSE:
					fds[0].fd = fds[0].fd == -1 ?
					    STDIN_FILENO : -1;
					break;
				case PARSE_HZ:
					ihz = parse.ts.tv_sec;
					break;
				case PARSE_QUIT:
					goto write_ret;
				}
			if (c == -1 && errno != EAGAIN &&
			    errno != EINTR)
				fds[1].revents |= POLLHUP;
		}
		if (cfd >= 0 && (fds[1].revents & POLLHUP))
			(void)close(cfd);
		if (cfd >= 0 && (fds[1].revents &
		    (POLLHUP | POLLERR | POLLNVAL))) {
			fds[1].fd = -1;
			if (cfile == NULL || fds[0].fd == -1)
				(void)close(STDIN_FILENO);
		}
	}
write_ret:
	af_close(a);
	free(b);
	return (EX_OK);
usage:
	c = strlen(getprogname()) + 8;
	(void)fprintf(stderr,
	    "usage: %s [-C command-fd] [-I info-fd] [-b buffer-size]\n"
	    "%*s[-c command-file] [-i info-file]\n", getprogname(), c,
	    "");
	return (EX_USAGE);
}

#ifdef SIGINFO
void
siginfo(int sig)
{
	/* XXXX2au: XXXXXXiB processed */
	char b[NAMEMAX + 2 + 8 + 11];
	size_t i;

	i = strlen(getprogname());
	if (i > NAMEMAX) {
		(void)memcpy(b, getprogname(), NAMEMAX - 3);
		(void)memcpy(b + NAMEMAX - 3, "...", 3);
		i = NAMEMAX;
	} else
		(void)memcpy(b, getprogname(), i);
	(void)memcpy(b + i, ": ", 2);
	i += 2;
	i += szwrite(b + i, info_bytes, "B", "iB");
	(void)memcpy(b + i, " processed\n", 11);
	i += 11;
	(void)write(STDERR_FILENO, b, i);
}
#endif

void
printts(int fd, struct timespec ts)
{
	char b[20]; /* >SSSSSSSSS.NNNNNNNNN */
	int i = sizeof(b);
	int n;

	if (fd < 0)
		return;
	if (ts.tv_sec > 999999999) {
		for (i = 1; i < 10; i++)
			b[i] = '9';
		b[0] = '>';
		write(fd, b, i);
	}
	if (ts.tv_nsec > 0) {
		for (n = 9; ts.tv_nsec % 10 == 0; ts.tv_nsec /= 10, n--)
			/* do nothing */;
		i -= n;
		while (ts.tv_nsec > 0) {
			b[i + --n] = ts.tv_nsec % 10 + '0';
			ts.tv_nsec /= 10;
		}
		while (n > 0)
			b[i + --n] = '0';
		b[--i] = '.';
	}
	do {
		b[--i] = ts.tv_sec % 10 + '0';
		ts.tv_sec /= 10;
	} while (ts.tv_sec > 0);
	(void)write(fd, b + i, sizeof(b) - i);
}

int
parsefd(int fd, struct parse *p)
{
	ssize_t r;
	int c;

	assert(p != NULL);
	for (;;) {
		if (p->index >= p->count) {
			if ((r = read(fd, p->buffer,
			    sizeof(p->buffer))) <= 0)
				return (r == 0 ? 0 : -1);
			p->index = 0;
			p->count = (int)r;
		}
		while (p->index < p->count) {
			c = p->buffer[p->index];
			switch (p->state) {
			case PARSE_DEFAULT:
				if (c == '=') {
					p->state = PARSE_SET;
					p->negative = 0;
				} else if (c == '-' || c == '+') {
					p->state = PARSE_NUMBER;
					p->negative = c == '-';
					p->whence = SEEK_CUR;
					p->ts.tv_sec = 0;
					p->ts.tv_nsec = 0;
				} else if (c >= '0' && c <= '9') {
					p->state = PARSE_NUMBER;
					p->negative = 0;
					p->whence = SEEK_SET;
					p->ts.tv_sec = c - '0';
					p->ts.tv_nsec = 0;
				} else if (c == '.') {
					p->state = PARSE_DECIMAL;
					p->negative = 0;
					p->whence = SEEK_SET;
					p->ts.tv_sec = 0;
					p->ts.tv_nsec = 0;
				} else if (c == '?') {
					p->index++;
					return (PARSE_INFO);
				} else if (c == 'P' || c == 'p') {
					p->index++;
					return (PARSE_PLAYPAUSE);
				} else if (c == 'Q' || c == 'q') {
					p->index++;
					return (PARSE_QUIT);
				}
				break;
			case PARSE_SET:
				if (c == '-' || c == '+') {
					p->state = PARSE_NUMBER;
					p->whence = c == '-' ?
					    SEEK_END : SEEK_SET;
					p->ts.tv_sec = 0;
				} else if (c >= '0' && c <= '9') {
					p->state = PARSE_NUMBER;
					p->whence = SEEK_SET;
					p->ts.tv_sec = c - '0';
				} else if (c == '.') {
					p->state = PARSE_DECIMAL;
					p->whence = SEEK_SET;
					p->ts.tv_sec = 0;
					p->ts.tv_nsec = 0;
					p->decimals = 9;
				} else if (c != '=') {
					p->state = PARSE_DEFAULT;
					p->index--;
				}
				break;
			case PARSE_NUMBER:
				if (c >= '0' && c <= '9')
					p->ts.tv_sec = p->ts.tv_sec * 10
					    + (c - '0');
				else if (c == '.') {
					p->state = PARSE_DECIMAL;
					p->decimals = 9;
					p->ts.tv_nsec = 0;
				} else if (c == 'h') {
					p->state = PARSE_ZEE;
				} else if (p->ts.tv_sec == 0 &&
				    p->whence == SEEK_CUR) {
					p->state = PARSE_DEFAULT;
					p->index--;
				} else {
					p->state = PARSE_DEFAULT;
					if (p->negative)
						p->ts.tv_sec =
						    -p->ts.tv_sec;
					p->ts.tv_nsec = 0;
					return (PARSE_SEEK);
				}
				break;
			case PARSE_DECIMAL:
				if (c >= '0' && c <= '9') {
					if (p->decimals > 0) {
						p->ts.tv_nsec =
						    p->ts.tv_nsec * 10 +
						    (c - '0');
						p->decimals--;
					}
				} else if (p->ts.tv_sec == 0 &&
				    p->ts.tv_nsec == 0 &&
				    p->whence == SEEK_CUR) {
					p->state = PARSE_DEFAULT;
					p->index--;
				} else {
					p->state = PARSE_DEFAULT;
					for (; p->decimals > 0;
					    p->decimals--)
						p->ts.tv_nsec *= 10;
					if (p->negative) {
						p->ts.tv_sec =
						    -p->ts.tv_sec;
						p->ts.tv_nsec =
						    -p->ts.tv_nsec;
					}
					return (PARSE_SEEK);
				}
				break;
			case PARSE_ZEE:
				p->state = PARSE_DEFAULT;
				if (c == 'z')
					return (PARSE_HZ);
				break;
			}
			p->index++;
		}
	}
}
